/* 算法实现模板 */
import AlgorithmBase from "./AlgorithmBase";
import ReactDOM from 'react-dom';
import React from 'react';
import { txtToMatrix, loadDataset } from "../../common/Common";
import MatrixViewer from "../../visual/MatrixViewer";
import Matrix from "../../common/Matrix";
import MatView2D, {createRandomPointStyleDict} from "../../visual/MatView2D";
import Mat from "../../common/Mat";


class SVMHandWrittenNums extends AlgorithmBase{
  constructor(){
    super('SVM');

    // 服务器数据集链接地址
    this.data1Url = `${this.homepage}data/ml-in-action/ch6/testSet.txt`;
  }

  init(){
    this.clear();

    this.collect();
  }

  initDescriptionUI(){
    this.domElementList.description.innerText = String.raw`SVM的一般流程
    (1)收集数据:可以使用任意方法.
    (2)准备数据:需要数值型数据.
    (3)分析数据:有助于可视化分隔超平面.
    (4)训练算法:SVM的大部分时间都源自训练,该过程主要实现两个参数的调优.
    (5)测试算法:十分简单的计算过程就可以实现.
    (6)使用算法:几乎所有分类问题都可以使用SVM,值得一提的是,
    SVM本身是一个二类分类器,对多类问题应用SVM需要对代码做一些修改.
    `;
  }

  // 准备数据
  initPrepareUI(){
    const prepareDiv  = document.createElement('div');
    const prepareTips = document.createElement('label');
    prepareTips.style.color = 'red';
    const btnPrepare  = document.createElement('button');
    btnPrepare.innerText = '数值化数据';
    btnPrepare.onclick = this.prepare.bind(this);

    this.domElementList.prepare.appendChild(btnPrepare);
    this.domElementList.prepare.appendChild(prepareTips);
    this.domElementList.prepare.appendChild(prepareDiv);

    this.prepareTips  = prepareTips;
    this.prepareDiv   = prepareDiv;
  }

  // 训练数据UI
  initTrainUI(){
    const btnTrain  = document.createElement('button');
    btnTrain.innerText = '训练';
    btnTrain.onclick = this.train.bind(this);

    const btnTrainAnimation  = document.createElement('button');
    btnTrainAnimation.innerText = '播放训练动画';
    btnTrainAnimation.onclick = this.trainAnimation.bind(this);

    const labelTrainTips     = document.createElement('label');

    this.domElementList.train.appendChild(btnTrain);

    this.domElementList.train.appendChild(btnTrainAnimation);
    this.domElementList.train.appendChild(labelTrainTips);
    this.labelTrainTips = labelTrainTips;
  }

  // 收集数据
  collect(url){
    if(!url) url = this.data1Url;
    fetch(url).then((response)=>{
      return response.text();
    }).then((txt)=>{
      this.dataset = txtToMatrix(txt);
      ReactDOM.unmountComponentAtNode(this.collectDiv);
      ReactDOM.render(
        <React.StrictMode>
          <MatrixViewer 
            matrix={this.dataset}
          />
        </React.StrictMode>,
        this.collectDiv
      );
      // 自动数值化
      this.prepare();    
    })
  }
  // 准备数据
  // 数值化数据并获取mat和label
  prepare(){
    const dataset = this.dataset;
    if(!dataset){
      this.prepareTips.innerText = '数据集未加载...';
      return;
    }

    this.trainDS = loadDataset(dataset);
    let headLabels = ['Mat'];
    for(let i = 0, il=this.trainDS.Mat[0].length; i < il; i++){
      headLabels.push(i);
    }
    headLabels.push('Label');

    ReactDOM.unmountComponentAtNode(this.prepareDiv);
    ReactDOM.render(
      <React.StrictMode>
        <MatrixViewer 
          matrix     = {Matrix.columnStack(this.trainDS.Mat, this.trainDS.Labels)}
          HeadLabels = {headLabels}
        />
      </React.StrictMode>,
      this.prepareDiv
    )   

    this.prepareTips.innerText = '数据准备完成.';

    // 自动显示数据
    this.analysis();
  }
  // 分析数据
  analysis(){
    const trainDS = this.trainDS;
    const pointStyleDict = createRandomPointStyleDict(trainDS.Labels);
    ReactDOM.unmountComponentAtNode(this.analysisDiv);
    ReactDOM.render(
      <React.StrictMode>
        <MatView2D 
          Mat     = {trainDS.Mat}
          Labels  = {trainDS.Labels}
          PointStyleDict = {pointStyleDict}
        />
      </React.StrictMode>,
      this.analysisDiv
    );
  
    this.pointStyleDict = pointStyleDict;
  }
  // SMO算法辅助函数
  selectJrand(i, m){
    let j = i;
    while(j === i){
      j = Math.floor(Math.random()*m);
    }
    return j;
  }
  clipAlpha(aj, H, L){
    if(aj > H){
      aj = H;
    }
    if(L > aj){
      aj = L;
    }
    return aj;
  }
  // do_gxi
  _do_gxi(X, y, i){
    let alphas = this.alphas;
    let gxi    = parseFloat(Mat.dot(
      Mat.multiply(alphas, y), 
      Mat.mul(
        X, 
        X.slice(i).transpose()
        )
      )) + this.b;
    
    return gxi;
  }
  // do_di
  _do_ei(X, y, i){
    let ei = this._do_gxi(X, y, i) - y[i];
    return ei;
  }
  // do selectj
  _do_selectj(i, m){
    let j = i;
    while(j === i){
      j = Math.floor(Math.random()*m);
    }
    return j;
  }

  // do clipalpha
  _do_clipalpha(alpha, H, L){
    if (alpha > H)
        alpha[0] = H;
    if (L > alpha)
        alpha[0] = L;
    return alpha;
  }
  // do eta
  _do_eta(X, i, j){
    let eta = 2 * Mat.dot(X.slice(i), X.slice(j)) - Mat.dot(X.slice(i), X.slice(i)) - Mat.dot(X.slice(j), X.slice(j));
    return eta;
  }
  // SMO简化版
  _do_smo(X, y ){
    let alphas   = this.alphas;
    let C        = this.C;
    let toler    = this.toler;
    let maxIters = this.maxIters;
    let m        = this.m;
    let iter     = 0;
  
    while(iter < maxIters){
      let alphaPairsChanged = 0;
      for(let i = 0; i < m; i++){       
        let Ei = this._do_ei(X, y, i);
        if(((y[i]*Ei < -toler) && (alphas[i] < C)) || ((y[i]*Ei > toler) && (alphas[i] > 0))){
          let j  = this._do_selectj(i, m);
          let Ej = this._do_ei(X, y, j);

          let alphaIold = parseFloat(alphas[i]);
          let alphaJold = parseFloat(alphas[j]);

          let L, H;
          if( parseFloat(y[i]) !== parseFloat(y[j])){
            L = Math.max(0, alphas[j][0] - alphas[i][0]);
            H = Math.min(C, C + alphas[j][0] - alphas[i][0]);
          }
          else{
            L = Math.max(0, alphas[j][0] + alphas[i][0] - C);
            H = Math.min(C, alphas[j][0] + alphas[i][0]);
          }
          if( L === H ){
            console.log('L===H');
            continue;
          }

          let eta = this._do_eta(X, i, j);
          if(eta >= 0){
            console.log("eta>=0");
            continue;
          }

          alphas[j][0] -= y[j] * (Ei - Ej) / eta;
          alphas[j]     = this._do_clipalpha(alphas[j], H, L);
          
          if((Math.abs(alphas[j] - alphaJold)) < 0.00001){
            console.log("j not moving enough");
            continue;
          }

          alphas[i][0] += parseFloat(y[j] * y[i] * (alphaJold - alphas[j]));

          const b1 = this.b - Ei - y[i] * (alphas[i] - alphaIold) 
                    * Mat.dot(X.slice(i), X.slice(i)) - 
                    y[j] * (alphas[j] - alphaJold) * Mat.dot(X.slice(i), X.slice(j));

          const b2 = this.b - Ej - y[i] * (alphas[i] - alphaIold) 
                    * Mat.dot(X.slice(i), X.slice(j)) - 
                    y[j] * (alphas[j] - alphaJold) * Mat.dot(X.slice(j), X.slice(j));

          if ((0 < alphas[i][0]) && (C > alphas[i][0])){
            this.b = b1;
          }
          else if ((0 < alphas[j][0]) && (C > alphas[j][0])){
            this.b = b2;
          }
          else{
            this.b = (b1 + b2) / 2;
          }
          
          alphaPairsChanged += 1
          console.log(`iter: ${iter} i: ${i}, paris changed ${alphaPairsChanged}`); 
          /////////////////////////////////////////////////////////////////////////
          // 保存训练历史记录-用于动画展示
          let svs = {};
    
          for(let i=0, il = alphas.length; i < il; i++){
            if(alphas[i] > 0){
              svs[i] = 1;
            }
          }

          let w = Mat.mul(Mat.multiply(alphas, y).T(), X)[0];
          this.trainHistory.push({svs:svs, w:w, b:this.b, time:new Date()});
          /////////////////////////////////////////////////////////////////////////
        }
      }
      if (alphaPairsChanged === 0){
        iter+=1;
      }
      else{
        iter = 0;
      }
      console.log(`iteration number: ${iter}`);
    }

  }

  // 更新分割超平面
  updateSeparator(sv, w, b){

    ReactDOM.unmountComponentAtNode(this.analysisDiv);
    ReactDOM.render(
      <React.StrictMode>
        <MatView2D 
          Mat       = {this.trainDS.Mat}
          Labels    = {this.trainDS.Labels}
          PointStyleDict = {this.pointStyleDict}
          Separator = { {type:'line', w:w, b:b} }
          HighLightPoints = {sv}
        />
      </React.StrictMode>,
      this.analysisDiv
    );
  }

  // 训练算法
  train(){
    const trainDS = this.trainDS;

    let X         = new Mat.Mat(trainDS.Mat);
    let y         = new Mat.Mat(trainDS.Labels);

    this.toler    = 0.001;
    this.C        = 0.6;
    this.maxIters = 40;
    this.m        = X.shape[0];
    this.alphas   = Mat.zeros([X.shape[0], 1]);
    this.b        = 0;

    // 训练过程保存
    this.trainHistory = [];
    let beginTime = new Date();
    this._do_smo(X, y);
    let endTime   = new Date();
    const dt = endTime - beginTime;

    this.labelTrainTips.innerText = `训练结束，用时:${dt}ms`;

    const lastest = this.trainHistory[this.trainHistory.length - 1];
    this.updateSeparator(lastest.svs, lastest.w, lastest.b);
  }

  trainAnimation(){
    if(this.trainHistory && !this.animation){
      this.trainHistoryIndex = 0;
      this.animation  = setInterval(this.playHistory.bind(this), 100);
    }
  }

  playHistory(){
    if(this.trainHistoryIndex < this.trainHistory.length){
      const lastest = this.trainHistory[this.trainHistoryIndex];
      this.updateSeparator(lastest.svs, lastest.w, lastest.b);
      this.labelTrainTips.innerText = `训练时间:${lastest.time.toString()}`;
      this.trainHistoryIndex += 1;
    }else{
      clearInterval(this.animation);
      this.animation = '';
      this.labelTrainTips.innerText = `播放完毕`;
    }
  }
}


export default SVMHandWrittenNums;